Chapter 17
Scripting Your MFC Application

by Kenn Scribner

In This Chapter

  Scripting Basics 626
  Object Models 637
  Implementing a Scripted Application 639

If there is one area where mixing MFC and COM is truly exciting (there are many), it has to be in adding scripting capability to your MFC application. Many MFC applications exist, as do many COM objects. Many MFC applications use COM objects. But few applications you find, except those coming from Microsoft, truly give you the ability to customize and automate their use through scripting. Frankly, many people I’ve talked with find this topic very mysterious. Yet it is one of the more interesting.

What is so exciting about scripting is the manner in which the application (MFC) and the COM objects work together in more than just a simple client/server relationship. To be truly useful, the COM object needs to be a part of the application yet still satisfy the requirements of COM. This can be challenging to implement.

In this chapter, I show you how to add scripting capabilities to your application. My techniques are not the only way this can be done, but I have found these techniques to be as faithful to COM as is possible yet still allow the COM objects to expose information private to the application. I’ll start with some basic concepts.

Scripting Basics

When I use the term scripting, I truly mean anointing your application with the capability to parse and execute VBScript or JScript code designed to access portions of your application. Perhaps your script will change the size of the application’s window. Or just as likely, your script might be designed to take some action after the application’s document has been modified (I refer to this state as dirty). You might also want a script available to quit the application when a predetermined set of criteria is met.

Your goal, as the application designer, is to think like your users and provide them with the best possible automation tools your application can provide. By this I mean that you begin your scripting architectural design process by laying out an object model for your application (I’ll cover this in more detail later in the chapter). When your users write a script for your application, they will be accessing objects your application exposes. The trick is to understand your application’s problem domain well and design an object model that best fits that problem domain.

Any objects your application exposes will have certain requirements levied upon them due to the nature of the Microsoft scripting architecture, which inherits its needs from Visual Basic. After all, VBScript is a language subset of Visual Basic. Microsoft used the same technology when it implemented Visual J++ and its scripting language subset JScript. To be sure you have an understanding of these architectural requirements, I’ll detail them later in this section.


Note:  

Throughout the remainder of this chapter, I will refer primarily to VBScript. This isn’t to neglect JScript, but rather to simplify the conceptual descriptions by referring solely to a single scripting language. When it comes to ActiveX scripting, either language is interchangeable from a scripting engine perspective.


Adding scripting capability to your MFC application requires you to provide scripting functionality in three major areas:

  The application itself and its object model
  The objects that fit into the application’s object model
  The script engine plumbing (such as parsing, execution, and so on)

Fortunately, you need only concentrate on the first two areas. Microsoft has provided the third free of charge, although you do have to license the technology and acknowledge Microsoft in your application. As an MFC programmer you already manage the first task, so the only added work on your part is to design the application’s object model and write the code to support the objects themselves. I’ll begin my detailed description of the scripting process by describing the piece Microsoft provides.

Scripting Interfaces

When you add scripting capability to your MFC application, you have several alternatives. First, you might design a completely proprietary scripting language and the tools that go with it (runtime environment, parsing, execution, and so on). Second, you might fake a scripting environment by providing some customization and pseudo-executable commands that might appear to be script-like. Of course, you could purchase a third-party package that provides most of the tools you need. But the alternative I find most attractive is using freely available scripting technology. I’m referring to the ActiveX Scripting engine provided by Microsoft.

Microsoft makes scripting available to you, the application programmer, by providing several COM objects and interfaces. If you know something about the scripting libraries and the COM interfaces, and if you have the compiler tools to support it (header files and such things), you can add scripting functionality to your application by invoking COM and using the scripting objects already available. And best of all, they’re free. Therefore, incorporating the ActiveX Scripting engine into your application is what this chapter really explores.

Microsoft ActiveX Scripting

The scripting technology you’ll use in this chapter is called Microsoft ActiveX Scripting. This technology provides you with the scripting runtime environment, a small set of development files, and a brief text file describing what is included in the download you will receive when you agree to the online licensing agreement (more on this later). The primary language files are VBScript.dll and JScript.dll, and the scripting runtime is provided by scrrun.dll. Each of these files is a COM in-process server DLL.

These DLLs collectively expose many COM interfaces, and describing them all is well beyond the scope of this chapter. However, the good news is that there are only a few you need to understand to provide basic scripting services. Microsoft has provided what I consider the four main scripting engine interfaces: IActiveScript, IActiveScriptParse, IActiveScriptSite, and IActiveScriptSiteWindow, and these are the four scripting interfaces I’ll deal with in this chapter. There are additional ActiveX scripting interfaces you might find interesting, so be sure to refer to the online documentation for more details.

The IActiveScript interface is used to initialize and control the scripting engine. For example, you add scripted objects from your object model using IActiveScript::AddNmedItem(). Or you terminate the engine and any running script using IActiveScript::Close().



IActiveScriptParse provides the methods you need to parse and execute a script. This interface is necessary because VBScript itself has no prescribed editing environment (unlike Visual Basic). Therefore, a simple VBScript file has no mechanism for loading itself from a file—this is something you provide in your application. Because of this, there has to be a mechanism in place to accept VBScript text and provide it to the scripting engine for parsing and execution. This role is shared by your application and IActiveScriptParse::ParseScriptText(), which is the main method you use for executing scripts.

Your application, as the host for scripted elements, must provide some basic plumbing to enable the scripting engine to do its work. You provide this by supplying implementations for the IActiveScriptSite interface. The most critical method in this interface is IActiveScriptSite::GetItemInfo(), which the engine calls to retrieve type information from your scripted objects. I’ll cover type information and type libraries in more detail later in this chapter.

The final of the four primary scripting interfaces is IActiveScriptSiteWindow. This interface has only two methods and is relatively simple, but it’s required because the scripting engine must have a way to retrieve a window handle if the engine has to display information to the user (as with an error, for example). The IActiveScriptSiteWindow::GetWindow() method provides for just this. As you might expect, applications with no user interface don’t support IActiveScriptSiteWindow. In this chapter, though, where your MFC application is hosting the scripting environment, its implementation is mandatory.

Now that you’ve been introduced to the COM interfaces your application will deal with, it’s time to actually retrieve the files you’ll need to incorporate scripting into your application. As always, there is much more interface-specific information contained within the online files.

Licensing

For you to integrate the Microsoft ActiveX Scripting engine into your application, you must agree to a licensing arrangement and acknowledge Microsoft’s scripting engine in your application. The licensing agreement and scripting runtime components are available at the Microsoft Internet site, http://msdn.microsoft.com/scripting/default.htm?/scripting/vbscript. This site works for both VBScript and JScript, as the scripting runtime engine handles both languages.

After you’ve provided some basic information, such as your name and that of your company, you’ll download the scripting engine and some necessary development tools (header files and such). Save these! You’ll need some portions of this download to compile your code, and other portions are used on your user’s systems to upgrade (or install for the first time) the files the scripting engine will require to execute.

You’ll also be asked to acknowledge Microsoft as the provider for the scripting engine on your About dialog box. Microsoft doesn’t specify precisely what you must say, only that you acknowledge Microsoft’s contribution to your application. For this chapter’s sample application, I created the dialog you see in Figure 17.1.


Figure 17.1  Sample application About dialog box.

Scripting Engine Implementation Details

When you have the requisite header files, msscript.h and activscp.h (from the download in the previous section), you have what you need to develop your basic scripting functionality. Of the four interfaces I mentioned previously, two are interfaces you call (IActiveScript and IActiveScriptParse), and two are interfaces you must provide for the scripting engine to call (IActiveScriptSite and IActiveScriptSiteWindow). As it happens, the implementations are relatively simple to implement, and in many cases you can use your code in many other scripted applications with little to no change.

The sample program provided with this chapter contains a COM object I named ScriptObject, as it is the COM object I use to marry the MFC application to the scripting engine. ScriptObject uses as its source a file named ScriptImpl.h, the code for which you see in Listing 17.1. This code provides the majority of the implementation of the IActiveScriptSite and IActiveScriptSiteWindow interfaces (the IActiveScriptSite::GetItemInfo() method is implemented elsewhere due to the nature of ATL).

Listing 17.1 IActiveScriptSite and IActiveScriptSiteWindow Implementation Using ATL Templates


// ScriptImpl.h : Implementation of IActiveScript* interfaces

#ifndef __SCRIPTIMPL_H_
#define __SCRIPTIMPL_H_

// IActiveScript* definitions
#include <activscp.h>

// swprintf definition
#include <stdio.h>

class ATL_NO_VTABLE IActiveScriptSiteImpl : public IActiveScriptSite
{
   STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObject) = 0;
   ATL_DEBUG_ADDREF_RELEASE_IMPL(IActiveScriptSiteImpl)

   // This method *must* be overridden in your implementation
   // (hence the E_FAIL).
   STDMETHOD(GetItemInfo)(LPCOLESTR pstrName, DWORD dwReturnMask,
      IUnknown** ppiunkItem, ITypeInfo** ppti)
   {
      // You must override this and provide your own implementation.
      return E_NOTIMPL;
   }
   STDMETHOD(OnScriptError)(IActiveScriptError *pscripterror)
   {
      // Set up the exception record
      EXCEPINFO ei;
      ::ZeroMemory(&ei,sizeof(ei));
      // Determine from where the error came
      DWORD dwCookie;
      LONG nChar;
      ULONG nLine;
      pscripterror->GetSourcePosition(&dwCookie, &nLine, &nChar);

      // Retrieve the source line
      BSTR bstrSourceLine = NULL;
      pscripterror->GetSourceLineText(&bstrSourceLine);

      // Fill the exception record
      pscripterror->GetExceptionInfo(&ei);

      // Create an output string
      OLECHAR wszMsg[2048];
      swprintf(wszMsg, OLESTR(“%s\n[Line: %d] %s\n%s”),
               ei.bstrSource, nLine, ei.bstrDescription,
               bstrSourceLine ? bstrSourceLine : OLESTR(“”));

      // Display the error string
      ::MessageBoxW(GetDesktopWindow(),
                    wszMsg,
                    L“Script Error”,
                    MB_SETFOREGROUND);

      // Free your BSTRs
      ::SysFreeString(bstrSourceLine);
      ::SysFreeString(ei.bstrSource);
      ::SysFreeString(ei.bstrDescription);
      ::SysFreeString(ei.bstrHelpFile);

      return S_OK;
   }

   STDMETHOD(GetLCID)(LCID *plcid)
   {
      // Change the LCID value to meet your internationalization
      // requirements...
      *plcid = MAKELCID(LANG_USER_DEFAULT,SORT_DEFAULT);
      return S_OK;
   }

   STDMETHOD(GetDocVersionString)(BSTR *pbstrVersion)
   {
      *pbstrVersion = SysAllocString(L“”);
      return S_OK;
   }

   STDMETHOD(OnScriptTerminate)(const VARIANT *pvr,
   Äconst EXCEPINFO *pei)
   {
   return S_OK;
   }

   STDMETHOD(OnStateChange)(SCRIPTSTATE ssScriptState)
   {
      return S_OK;
   }

   STDMETHOD(OnEnterScript)(void)
   {
      return S_OK;
   }

   STDMETHODIMP OnLeaveScript(void)
   {
      return S_OK;
   }
};

class IActiveScriptSiteWindowImpl : public IActiveScriptSiteWindow
{

   STDMETHOD(QueryInterface)(REFIID riid, void ** ppvObject) = 0;
   _ATL_DEBUG_ADDREF_RELEASE_IMPL(IActiveScriptSiteImpl)
   STDMETHODIMP GetWindow(HWND *phwnd)
   {
      *phwnd = GetDesktopWindow();
      return S_OK;
   }

   STDMETHODIMP EnableModeless(BOOL)
   {
      return S_OK;
   }
};

#endif // __SCRIPTIMPL_H_



You will also need to instantiate the scripting engine itself to retrieve the IActiveScript and IActiveScriptParse interfaces. In the sample program, I activate the scripting engine in CScriptObject::FinalConstruct(), as shown in Listing 17.2. If you look at the actual CoCreateInstance(), you’ll see I use a class identifier of CLSID_VBScript. This activates the VBScript.dll COM in-process server and allows me to provide VBScript services to my application’s users. However, I could have instead (or in addition to) requested the JScript version by using CLSID_JScript. The choice is yours. I used VBScript because it’s the scripting language of Developer Studio and many other Microsoft products, so I know my application’s users will be somewhat familiar with that language.

Listing 17.2 IActiveScript and IActiveScriptParse Instantiation


HRESULT CScriptObject::FinalConstruct()
{
   // CoCreate the scripting engine
   HRESULT hr = CoCreateInstance(CLSID_VBScript,
                                 NULL,
                                 CLSCTX_ALL,
                                 IID_IActiveScriptParse,
                                 reinterpret_cast<LPVOID*>
                                 Ä(&m_pScriptParser));
   if ( SUCCEEDED(hr) ) {
      // Query for the script interface
      CComQIPtr<IActiveScript,&IID_IActiveScript>
      ÄpScript(m_pScriptParser);
      if ( pScript.p != NULL ) {
         // Assign our attribute
         m_pScript = pScript;

         // Initialize the script engine
         hr = m_pScriptParser->InitNew();
         if ( SUCCEEDED(hr) ) hr = m_pScript->SetScriptSite(this);
         if ( SUCCEEDED(hr) ) hr =
         Äm_pScript->SetScriptState(SCRIPTSTATE_STARTED);
      } // if
      else {
         // Failed...
         hr = E_FAIL;
      } // else
   } // if

   return hr;
}

The mechanics you’ve seen so far are aimed at bringing the scripting engine into your application. I’ll provide much more detail later in the chapter when I analyze the sample program. Before I do that, though, you should have some idea what your application must provide the scripting engine to make things work as they should. Therefore, a little COM background is in order.

Dual Interfaces

When you have the scripting engine and the scripting plumbing your application will use in place, you can turn to developing your application’s objects. Before I discuss that topic, which is the topic of the next section, I will introduce some key concepts and terms you must be aware of when developing these objects.

The first concept you must understand is the nature of the COM dual interface. This is a COM interface like many others, but the major difference is that it inherits directly from the IDispatch interface rather than from the more common IUnknown interface. The significance of this is that the interfaces you develop for your scripted objects have a split personality. On the one hand, they have a custom interface, which is the type of COM interface C++ programmers traditionally deal with. This is the traditional IUnknown interface. On the other hand, though, they have this other IDispatch nature. As it happens, it’s the IDispatch side of their behavior that is required for scripting, and the key lies in the concept of late binding.

Late Binding

To me, the easiest way to describe early versus late binding is to think in terms of compilation and execution timelines. That is, if you are writing a traditional C++ program, you undoubtedly are linking in static libraries (the C runtime library, for example). Because the executable will have access to the functionality of the libraries (they’re compiled into the executable), they are bound to the executable before it actually runs. This is early binding. Traditional linking (versus dynamic linking) places the library code in your executable’s image for when the executable runs.

On the other hand, when you run a script, what you are really doing is submitting your script text to a script engine (otherwise known as an interpreter). The script engine cannot be given prior knowledge regarding the precise needs of your script because it is a general purpose engine—it knows only which methods you call and in what objects you call them when it encounters the code in the script. The script is interpreted. This is very much like reading a book. You only know how the story goes when you actually read the pages.

Therefore, the script engine has to have some other mechanism for querying the objects it has access to when it encounters them while interpreting the script. This query process is known as late binding because the script is already executing before the script engine knows what libraries to access.

Microsoft provides for late binding with something known as a type library. If you’ve read Chapter 16, “Using MFC and ATL,” you were introduced to the Interface Description Language, or IDL. The IDL file describes the COM interface by codifying the methods and properties the interface supports as well as the parameter lists each function requires. A type library is a tokenized form of IDL (some information is removed, unfortunately). When the script engine parses a script that accesses a given object’s method, the script engine retrieves the type library for that object and compares what it finds there to what the script actually requested. If there is a match in function signatures, the script engine executes the object’s method on behalf of the script. On the other hand, if the function signatures don’t match (or there is some other error), the scripting engine halts the script with an error message to the user.

Dispinterfaces

Given the preceding discussion, assuming the script requested a particular object’s method and the script engine found a matching function signature in the object’s type library, the scripting engine will execute the method. But in this case the script engine cannot simply execute compiled code. After all, the script engine in all likelihood wasn’t compiled with your particular object in mind. It was compiled, however, with a generic means of executing object methods. How it executes those methods is through the use of the IDispatch::Invoke() method. When the script engine calls IDispatch::Invoke(), it is using a dispinterface (short for dispatch interface).

In this case, the object’s methods are enumerated. That is, each object method is assigned a number at compilation time (in both the IDL and C++ source files). The script engine passes the associated number to the object for execution through Invoke(). In this fashion, the script engine merely needs to support IDispatch. It can handle an infinite number of different interfaces, so long as each interface is callable through Invoke(). The object’s type library informs the script engine which method number is associated with what method, and then the script engine passes that number to the object through Invoke(). The object receives the number, through the mechanics of Invoke(), and then executes the associated method on behalf of the script and script engine. This pseudo-interface is the dispinterface.


Note:  

I mentioned that an infinite number of interfaces could be supported through IDispatch::Invoke(). That is not to say, however, that a single interface could have an infinite number of methods. On the contrary, the current limits are 512 methods for Windows 95 and Windows 98, and 1024 for Windows NT 4, SP3.




Properties

Scriptable objects also typically support properties. A property is akin to a C++ attribute. Say, for example, that an object in your application’s object model draws text. That object might logically have a font property that tells the object what typeface to use. Related properties might include font size, face, and color.

Objects implemented with a dual interface, however, treat properties in one of two ways, depending upon your point of view. To your script, the property is simply an attribute of an object. You set and retrieve the property value based upon the context of the script text. Using an imaginary font object as an example, this VBScript code would retrieve the text color:

color = FontObject.color

It would similarly set a new text color by placing the object and property on the left-hand side of the assignment:

FontObject.color = 255

However, to the COM object the script is accessing, the property is an attribute with access limited by get and put accessor functions. That is, the object actually implements a property as an attribute and a method pair. One method puts the new attribute value, and the other gets it. For example, to the COM object itself, the property is a combination of the following:

protected:
   COLORREF m_clrFont;

public:
   HRESULT get_Color(VARIANT* newVal);
   HRESULT put_Color(VARIANT newVal

The object encapsulates the actual property. The script is allowed a shorthand notation for property access, but the COM object always implements data hiding. The property itself is never actually exposed to the script.


Note:  

What this means is this: the object’s developers provide for scripted properties by adding methods to their object. This is completely hidden from the script itself. To the script, the property appears to be a single, public value.


You will see these concepts in action later. The objects to which I’ve been referring collectively work together to provide the script access to your application’s infrastructure. This collection of objects is called an object model, and this topic merits some discussion. Proper object model design is critical to your application’s ease of use and logical architectural design.

Object Models

Before I get started, I must confess that the sample program’s object model isn’t an ideal model for a text editor. My goal with the sample program was to demonstrate one technique for introducing scripting into an MFC application, not to demonstrate some arbitrarily limited object architecture. The final form of my object model isn’t what is important here. How I derived it is, however.

There are probably as many definitions of object model as there are programmers. But in this case, I loosely define the term object model to mean scripting architecture. Your application’s object model is the view your users see of your architecture when they look at it from a scripting perspective. Each of these objects will have properties, methods, and exposed events. Through a script, your user will be able to manipulate the properties, methods, and events to combine functionality from one or many objects into a single action (the running script).

For example, if your application were a paint program, you might design an object model something like that shown in Figure 17.2. Then again, you might add objects, or remove some of the objects I’ve added.


Figure 17.2  One possible paint program object model.

In this object model, the application object would manage the MFC application itself. Perhaps you could manipulate the frame window (or add a window object for that purpose). Or you could quit the application.

The canvas object would contain the actual bitmap. It would do such things as load and save a bitmap, manage cut-and-paste operations, and it would have subordinate objects that could manipulate important aspects of the bitmap, such as detailed color management (palette object), bitmap flipping and rotation (special effects), and pen, brush, and shape management.

When this application’s user writes a script, she would (presumably) be able to clear the canvas, create a shape, draw the shape, apply a color to the shape, and draw lines over the shape. She then might be able to save the bitmap (contained by the canvas) to disk and print a copy.

This particular architecture tells me many things. For one, for any application object, there is a single canvas object. This is a single document (SDI) application. If it had multiple canvas objects, or if it indicated there was a canvas object collection from which you retrieved a particular canvas object, it would be a multiple document (MDI) application. I also see that for this canvas object, there is a single object for special effects, pen, and so on. At any one time, then, you can apply only a single special effect or place one shape. This isn’t necessarily bad—it’s a design decision you make when you lay out the object model.

Some objects are notably missing, which could have been by omission or by design (it’s sometimes hard to tell). For example, there is no obvious mechanism for writing text onto the canvas. Granted, Figure 17.2 doesn’t provide tremendous detail. You don’t see the properties, methods, and events each object implements. The application’s user is the person best qualified to tell you whether the omission of a text and font object is a good or poor design decision.

The point of this exercise is this: Design an object model. And when you design your application’s object model, think like your users and ask yourself, “What would I want to automate using this application?” This should naturally lead you to design some basic objects. Then, as you imagine scripting scenarios, add to and modify your design as necessary to provide the means to adequately automate your application from your user’s perspective.

There is no hard and fast rule to guide you through this process, though a software engineering background will be helpful. But just as you can tell a good job from a poor job, generally you will know a good object model from a poor one, even if the programmer in the next office has a different opinion. In any case, when you have your application’s object model designed, it’s time to implement the objects in code.

Implementing a Scripted Application

In this section I’ll describe the practical aspects of adding scripting to your application. This chapter’s sample program consists of the simple text editor you see in Figure 17.3. In fact, it uses a CEditView view class, so actually creating the application couldn’t have been easier. I simply created a new SDI application and selected CEditView as its view class. After the AppWizard had generated the code and I compiled what it gave me, I had a fully functional text editor.


Figure 17.3  The scripted text editor.



For demonstration purposes, I designed a very simple object model for the text editor. This object model consists of three objects: the application object, the document object, and a selected text object. Their descriptions follow.

The following are the specifications of the application object:

  Properties

Document—Retrieves the document object

Height—Frame window height

Width—Frame window width

Top—Frame window top coordinate

Left—Frame window left coordinate

WindowState—Frame window state (minimized, and so on)

Caption—Frame window caption

  Method

Quit—Terminate the application

  Events

DocumentNew—A new document was created

DocumentOpen—A document was opened

DocumentSave—The document was saved

DocumentSaveAs—The document was saved with a new name

These are the specifications of the document object:

  Properties

Application—Retrieves the application object

TextSelection—Retrieves the text selection object

Dirty—Indicates the document has been modified

  Methods

Save—Saves the document to disk

SaveAs—Saves the document to disk with a new name

Open—Opens a document from disk

New—Creates a new document

Print—Prints the document

Clear—Clears (deletes) all document text

  Event

IsDirty—Fired when document becomes dirty

The following are the specifications of the text selection object:

  Properties

Application—Retrieves the application object

Document—Retrieves the document object

UpperSelBound—Upper selection character index

LowerSelBound—Lower selection character index

  Methods

GetCursorPos—Retrieves the cursor position

SetCursorPos—Moves the cursor to a specific position

Cut—Cuts the selection to the clipboard

Copy—Copies selection to the clipboard

Paste—Pastes from clipboard

SelectAll—Selects all document text

SelectRange—Selects a text range

ClearAll—Clears all document text

ClearSelection—Clears selected text

InsertText—Inserts text at cursor

  Event

KeyPressed—Fired when a character is entered into the edit control

Given this object model, it is time to examine how the objects were developed.

Object Implementation

I chose to implement the objects in ATL using the following recipe. Unfortunately, it’s beyond the scope of this chapter to examine each object in detail. The basic thought behind each is the same, though. I created a simple ATL COM object with a dual interface and added methods and properties to give it the proper functionality. You need not use ATL, however. Any COM programming paradigm that enables you to implement a dual interface will work just fine, to include MFC. In this case, though, I followed the steps outlined in the next section.

ATL Object Creation

Creating an ATL COM DLL is very much like creating the local server object you saw in Chapter 16. The main difference is that you create an ATL project from scratch rather than add ATL support to an existing MFC project. In any case, here are the basic steps:

  Create the basic ATL (DLL) project.
  Add an ATL simple object.
  Name the object and assign the attributes (be sure it has a dual interface and supports connection points and error information).
  In the source code, add support for object safety (this assists with security so warning dialogs don’t appear each time the user runs a script).
  Add all the methods and properties required, including those methods for events (added to the event interface).
  Compile the code to create a type library.
  Add the connection point table using the Connection Point Wizard (this step requires the type library you just created).
  Add the required attributes (pointers to parent objects, and so on).
  Add (iteratively) the meat to each of the methods.

If you’re unsure how to create the ATL COM objects, please refer to a good ATL COM programming book. Using the text selection object as an example, Listing 17.3 shows you the IDL file I created. When you examine Listing 17.3, look especially at the IDL you see versus the methods and properties I described previously. They should match up quite well.

Listing 17.3 The Text Selection Object’s IDL Definitions


// TextObject.idl : IDL source for TextObject.dll
//

// This file will be processed by the MIDL tool to
// produce the type library (TextObject.tlb) and marshaling code.

import “oaidl.idl”;
import “ocidl.idl”;
   [
      object,
      uuid(1E1B8551-B15D-11D2-B235-00C04FBEDB8F),
      dual,
      helpstring(“ITextSelObject Interface”),
      pointer_default(unique)
   ]
   interface ITextSelObject : IDispatch
   {
      [propget, id(1), helpstring(“property Application”)]
      ÄHRESULT Application([out, retval] VARIANT *pVal);
      [propget, id(2), helpstring(“property Document”)]
      ÄHRESULT Document([out, retval] VARIANT *pVal);
      [propget, id(3), helpstring(“property UpperSelBound”)]
      ÄHRESULT UpperSelBound([out, retval] VARIANT *pVal);
      [propput, id(3), helpstring(“property UpperSelBound”)]
      ÄHRESULT UpperSelBound([in] VARIANT newVal);
      [propget, id(4), helpstring(“property LowerSelBound”)]
      ÄHRESULT LowerSelBound([out, retval] VARIANT *pVal);
      [propput, id(4), helpstring(“property LowerSelBound”)]
      ÄHRESULT LowerSelBound([in] VARIANT newVal);
      [id(5), helpstring(“method SetApplication”), hidden]
      ÄHRESULT SetApplication([in] LPDISPATCH newVal);
      [id(6), helpstring(“method SetDocument”), hidden]
      ÄHRESULT SetDocument([in] LPDISPATCH newVal);
      [id(7), helpstring(“method GetCursorPos”)]
      ÄHRESULT GetCursorPos([out] VARIANT *pLineVal,
      Ä[out] VARIANT *pIndexVal);
      [id(8), helpstring(“method SetCursorPos”)]
      ÄHRESULT SetCursorPos([in] VARIANT lineVal,
      Ä[in] VARIANT indexVal);
      [id(9), helpstring(“method Cut”)] HRESULT Cut();
      [id(10), helpstring(“method Copy”)] HRESULT Copy();
      [id(11), helpstring(“method Paste”)] HRESULT Paste();
      [id(12), helpstring(“method SelectAll”)] HRESULT SelectAll();
      [id(13), helpstring(“method SelectRange”)]
      ÄHRESULT SelectRange([in] VARIANT lowerVal,
      Ä[in] VARIANT upperVal);
      [id(14), helpstring(“method ClearAll”)] HRESULT ClearAll();
      [id(15), helpstring(“method ClearSelection”)]
      ÄHRESULT ClearSelection();
      [id(16), helpstring(“method InsertText”)]
      ÄHRESULT InsertText([in] VARIANT newVal);
      [id(17), helpstring(“method InitHwnd”), hidden]
      ÄHRESULT InitHwnd([in] OLE_HANDLE hWnd);
   };

[
   uuid(1E1B8545-B15D-11D2-B235-00C04FBEDB8F),
   version(1.0),
   helpstring(“TextObject 1.0 Type Library”)
]
library TEXTOBJECTLib
{
   importlib(“stdole32.tlb”);
   importlib(“stdole2.tlb”);

   [
      uuid(1E1B8553-B15D-11D2-B235-00C04FBEDB8F),
      helpstring(“_ITextSelObjectEvents Interface”)
   ]
   dispinterface _ITextSelObjectEvents
   {
      properties:
      methods:
      [id(1), helpstring(“event BeforeTextChange”)]
      ÄBOOL BeforeTextChange();
      [id(2), helpstring(“event TextChange”)] HRESULT TextChange();
   };

   [
      uuid(1E1B8552-B15D-11D2-B235-00C04FBEDB8F),
      helpstring(“TextSelObject Class”)
   ]
   coclass TextSelObject
   {
      [default] interface ITextSelObject;
      [default, source] dispinterface _ITextSelObjectEvents;
   };
};



There are several things to note when examining Listing 17.3, and most of these revolve around the requirements of scriptable interfaces you saw earlier in the chapter. The next section outlines the code required to meet the basic requirements I mentioned.

Meeting Scriptable Object Requirements

To briefly review, COM interfaces designed to work with a scripting environment must be dual interfaces (derived from IDispatch), must support late binding through type libraries, must expose events as dispinterfaces, and must pass parameters as VARIANTs. Let’s see how all of this is done in code.

Referring back to Listing 17.3, this code tells you the object implements a dual interface:

[
      object,
      uuid(1E1B8551-B15D-11D2-B235-00C04FBEDB8F),
      dual,
      helpstring(“ITextSelObject Interface”),
      pointer_default(unique)
   ]
   interface ITextSelObject : IDispatch
   {
      (properties and methods described here...)
   }

The object has the dual attribute set as well as clearly inheriting the IDispatch interface.

You also see the use of VARIANT datatypes for parameter values. Scripts use VARIANT datatypes because they are a discriminated union and therefore might contain any of a number of data values. Chapter 16 provides more information in this area.

Also note the differences in how properties are declared as compared to methods. The Document property, for example, is a particularly interesting property as it is a read-only property (there is no corresponding propput attribute):

[propget, id(2), helpstring(“property Document”)]
ÄHRESULT Document([out, retval] VARIANT *pVal);

I removed the corresponding property method to assign the document object pointer and replaced it with a hidden method:

[id(6), helpstring(“method SetDocument”), hidden]
Ä HRESULT SetDocument([in] LPDISPATCH newVal);

I did this because the script has no reason to set the document object pointer. To do so could cause horrific results, and none of the possible outcomes are good. The hidden attribute, by the way, makes the method unavailable to type library viewers such as Visual Basic. In effect, this method is private to the object, though there is no enforcement of that privacy (as there is with private C++ class attributes and methods).

The actual implementations for these two methods are also interesting to see, as there is some work involved with accessing the VARIANT. Listing 17.4 shows the code I use to return the document object pointer back to the script.

Listing 17.4 The Text Selection Object’s get_Document() Method


STDMETHODIMP CTextSelObject::get_Document(VARIANT *pVal)
{
   HRESULT hr = S_OK;
   try {
      // Check their pointer
      if ( pVal != NULL ) {
         // Clear their variant
         ::VariantClear(pVal);

         // Copy in our document’s IDispatch pointer
         VARIANT var;
         var.vt = VT_DISPATCH;
         var.pdispVal = m_pDocObject.p;
         ::VariantCopy(pVal,&var);
      } // if
      else {
         // Wasn’t a valid pointer
         hr = E_POINTER;
      } // if
   } // try
   catch (...) {
      // Some error...
      hr = E_FAIL;
   } // catch

   return S_OK;
}

Note that the IDL property Document was translated to be a method declared as STDMETHODIMP CTextSelObject::get_Document(VARIANT *pVal) when implemented in the C++ source file. As I mentioned, properties to the script are really methods to the COM object. In any case, when the script requests the document object’s pointer, I check for a valid return pointer, and if it is valid, I create a new VARIANT that I fill with the pointer this object contains. Then, after that has been completed, I copy the local VARIANT to the remote VARIANT for the script to interpret. Most of the remaining object properties and methods follow this vein. After the objects have been implemented, it is time to integrate them into the MFC application.

Adding the Scriptable Objects to Your MFC Application

At this point you have a basic text editor and a handful of COM objects. A good question you might be asking yourself is how do I intend to marry the COM objects to the application? This is the tricky part, in many cases. After all, what you’re asking is how to retrieve information from an executable and pass it to a DLL when the DLL requires the information. It’s easy to go the other way. When an application wants information from a DLL, it simply asks the DLL, through a function pointer, for the data. COM DLLs (in-process servers) are more than mere DLLs, however. Moreover, your objects will be reacting to directives from a running script and might require information or call methods that are otherwise private to the application.

There are several ways to attack this problem. I personally try to use the least intrusive method possible, and if that doesn’t work, move to a more intrusive approach. The mechanisms I see available are these, though undoubtedly there are additional ways to solve this problem:

1.  Pass an HWND to the COM object and use that HWND for SendMessage() calls for information and/or subclass the HWND (especially useful for event processing).
2.  Implement connection points in the application, so that when the COM object requires information it fires an event to retrieve the data.
3.  Expose methods and attributes in the MFC C++ classes and somehow pass a pointer to them to the COM object.

I use the first mechanism almost exclusively, even though it involves something COM normally would advise against—you have to somehow pass an HWND down to the COM object. This is typically discouraged in COM programming, as HWNDs are system-specific. If for some reason someone were to create your object and expect it to work in a distributed environment, they would be in for a shock. HWNDs cannot be passed over a network! However, this is the least distasteful alternative, as you’ll see.


Tip:  

Using this mechanism, there is no reason why you could not use RegsterWindowMessage() to create application-specific Windows messages. This would allow your COM objects to retrieve information private to your application easily. Simply send the message from your COM object, and add a handler to your MFC class.




The second alternative, implementing connection points, is not necessarily a bad alternative. It simply involves a lot more code and wiring to properly implement the connection point mappings in the MFC application. Depending upon your point of view, however, it might be easier than subclassing the application’s HWND.

The last alternative should rarely (if ever) be considered. For one thing, the C++ method you call will have to be static. Nonstatic methods have an implied this pointer, which you do not have when running in your COM object. Static methods themselves introduce other (typically) undesirable artifacts, such as a single static method/attribute for all instantiations of the class. It’s also difficult to access nonstatic class data. But the worst problem, by far, with this last mechanism is the fact you would be passing a local process pointer from the application to the COM object. Remember, COM will want to marshal everything (see Chapter 16), and your normal, everyday function/data pointer’s marshaling will be tenuous at best. The reasons for this are complex and steeped in COM lore. It’s best to avoid any temptations you might entertain with this mechanism. Don’t do it.

Now let’s examine how you set the document pointer in the first place. To reiterate, remember that the text selection COM object contains pointers to both the application and document objects. These pointers are IDispatch pointers, and you obtain them when you create the objects in the first place. After their creation, you must somehow provide to each object the other relevant pointers. I’ll discuss that process a bit further in the chapter. For now, Listing 17.5 shows you the code you would use to store a local IDispatch pointer.

Listing 17.5 Storing a Related Object’s IDispatch Pointer


STDMETHODIMP CTextSelObject::SetDocument(LPDISPATCH newVal)
{
   HRESULT hr = S_OK;
   try {
      // Accept new value
      if ( newVal == NULL ) {
         // Invalid pointer
         _ATLASSERT(newVal != NULL);
         hr = E_POINTER;
      } // if
      else {
         // Set the pointer
         m_pDocObject = newVal;
      } // else
   } // try
   catch (...) {
      // Some error...
      hr = E_FAIL;
   } // catch

   return hr;
}

The attribute m_pDocObject is declared using CComPtr, so the incoming object is automatically AddRef()’d (which is according to the rules of COM when passing in object pointers). If you’re not using CComPtr or some other smart pointer, don’t forget that AddRef(). You might find yourself at some time calling an object that was terminated without your knowledge or consent.

The code I use to perform this wiring feat is found in the application’s view class, in a helper function I called InitializeObjects(). You see this helper function in Listing 17.6.

Listing 17.6 Initializing the Object Model Objects


void CScriptView::InitializeObjects()
{
   try {
      // Create your scripting objects
      HRESULT hr = m_pDocObject.CreateInstance(CLSID_DocObject);
      if ( SUCCEEDED(hr) ) {
        // Initialize the document object
        CMainFrame* pFrame = (CMainFrame*)AfxGetMainWnd();
        ASSERT(pFrame != NULL);
        pFrame->AddDocObject(bstr_t(“document”),m_pDocObject);
        m_pDocObject->InitViewHwnd(reinterpret_cast<long>(m_hWnd));
        m_pDocObject->InitEditHwnd(reinterpret_cast<long>
        Ä(GetEditCtrl().m_hWnd));

        // Text selection object...
        hr = m_pTextSelObject.CreateInstance(CLSID_TextSelObject);
        if ( SUCCEEDED(hr) ) {
           // Initialize the document object
            pFrame->AddTextSelObject(bstr_t(“textselection”),
                                            m_pTextSelObject);
            m_pTextSelObject->InitHwnd(reinterpret_cast<long>
            Ä(GetEditCtrl().m_hWnd));

            // Wire up the objects
            m_pDocObject->SetApplication(pFrame->GetAppObject());
            m_pDocObject->SetTextSelection(m_pTextSelObject);
            m_pTextSelObject->SetApplication(pFrame->GetAppObject());
            m_pTextSelObject->SetDocument(m_pDocObject);
         } // if
         else {
            // Some error...
            AfxMessageBox(“Unable to create the text selection
            Äobject”,MB_OK|MB_ICONERROR);
         } // else
      } // if
      else {
         // Some error...
         AfxMessageBox(“Unable to create the document object”,
                       MB_OK|MB_ICONERROR);
      } // else
   } // try
   catch (...) {
      // Some error...
      AfxMessageBox(“Unable to create scripted objects”,
                    MB_OK|MB_ICONERROR);
   } // catch
}

This method is called by the frame window class (CMainFrame) when the frame is initially shown by CMainFrame::ActivateFrame(). The frame window class helper functions CMainFrame::AddDocumentObject() and CMainFrame::AddTextSelObject() are similar, though much more brief. Those functions have the added chore of adding the names of the scripted objects to the scriptable object name vector m_vScriptElements in the script object by using IScriptObject::AddObject().

Events

Another interesting topic is event generation. Scripts certainly can alter an object’s state by changing the object properties and calling object methods. But in many cases a script might like asynchronous information from the object. These are events. An example of an event could be when the user initially types something into the edit control. Before this time, the edit control has no text and is not dirty. After the keypress, the control contains modified text and is now dirty. If a script were to look for some sort of dirty event, the script could take action when notified the document was now dirty.

I manage this behavior in the objects by implementing connection points and subclassing the windows of interest. For example, I know when the edit control has dirty text because I subclass the edit control and intercept WM_CHAR messages, as well as several other messages that indicate a dirty status. The code shown in Listing 17.7, found in the document object, shows how such an event would be generated (fired).

Listing 17.7 Firing a Document-Is-Dirty Event


LRESULT OnChar(UINT, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
   if ( !m_bIsDirty ) {
      // If we haven’t already fired the event, do so
      // now...
      m_bIsDirty = TRUE;
      Fire_IsDirty();
   } // if
   // Allow the control to handle the character
   return m_CEditCtrl.DefWindowProc(WM_CHAR,wParam,lParam);
}

The function Fire_IsDirty() is part of the ATL connection point architecture. How it is created and how it works are beyond the scope of this chapter. (Refer again to a good COM book for more information, such as Sams Publishing’s COM/DCOM Unleashed.) However, it is nothing more than a method assigned to the document object event interface. It merely looks for all of the objects connected to its interface (through an internally kept list) and notifies each of the change in document status. The main concept to take from this is that events are fired based upon Windows messages you intercept when you subclass a given HWND.


Tip:  

Window subclassing is also beyond the scope of this chapter, though you’ll find MFC window subclassing discussed in Chapter 5, “Custom Control Development.” In ATL, however, you easily subclass windows using CContainedWindow. Simply create a variable of type CContainedWindow and use its SubclassWindow() method (passing in an HWND).


When you intercept a message of note, you fire the particular event that corresponds to the message. For example, had I wanted to fire an event each time the user pressed a key, this handler would intercept the WM_CHAR message and I’d fire my keypress event at that time.

Any Windows message you intercept is a candidate. I use this technique to determine when the user opened a new text file, for example. I subclass the MFC view window and intercept the WM_COMMAND message with the command identifier of ID_FILE_OPEN (defined in afxres.h). When I receive this message, from the subclassed window (the MFC view), I fire an event to signal a document has been opened. A script can then do with that information whatever it will.


Tip:  

For those interested in the ATL connection point code, if your events don’t fire as expected even after you’ve inserted the connection point proxy using the Connection Point Wizard, you probably need to include IProvideClassInfo2Impl in your object’s inheritance list. The scripting engine retrieves information from your object using IProvideClassInfo2. Don’t forget to add entries to your COM map for IProvideClassInfo and IProvideClassInfo2.


Script Management

This final section discusses what I call script management. By this, I mean your application is responsible for not only providing the objects that implement your object model, activating and using the scripting engine, and performing the tasks it was designed to perform: It also must have a mechanism for storing and accessing script text.

The sample application you’ve been examining takes a fairly common approach. The scripts themselves are stored as text files with a custom file extension. They must be located in the current working directory to be available to the application. I provide fairly robust code to locate the files and bring them from disk into memory, from where they can be used by either the script engine or my simple script editor.

The bottom line is that your application must also host the scripts as well as the conduit to the script engine. Helping your users manage their scripts helps them use your application more effectively, and ultimately that’s the best reason for adding scripting capability in the first place.

Summary

Hopefully, this brief look at MFC and scripting has whet your appetite, and you’ll give scripting a try when you implement your next MFC application. The programming challenges are intriguing—even fun. Be sure to begin your work by thinking like your users and designing an object model accordingly. The challenges, and the rewards, will be well worth your efforts.